package dpss.server;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

import javax.xml.parsers.ParserConfigurationException;

import org.omg.CORBA.ORB;
import org.omg.CORBA.ShortHolder;
import org.omg.CORBA.StringHolder;
import org.xml.sax.SAXException;

import dpss.commons.CustomLogger;
import dpss.commons.Player;
import dpss.commons.Repo;
import dpss.server.udp.UDPMessage;
import dpss_api_corba.DpssApiPOA;
import dpss_api_corba.DpssApiPackage.DpssException;

/**
 * The implementation of the DpssServer interface
 * 
 * @author musab.mirza
 * 
 */
public class DpssApiImpl extends DpssApiPOA {

	@SuppressWarnings("unused")
	private static CustomLogger logger = CustomLogger.getInstance();

	/**
	 * Reference to the Server's account db
	 */
	private AccountDatabase accountDatabase = null;

	/**
	 * Constructor to start the accounts databases
	 * 
	 * @throws RemoteException
	 */
	protected DpssApiImpl(AccountDatabase accountDatabase) {
		super();
		this.accountDatabase = accountDatabase;
	}

	@SuppressWarnings("unused")
	private ORB orb;

	@Override
	public synchronized void createPlayerAccount(String firstName,
			String lastName, short age, String username, String password,
			String ipAddress) throws DpssException {
		
		logger.log("Create player request with username: " + username + " from IP: " + ipAddress);

		username = username.trim();
		if (username.equalsIgnoreCase("admin")) {
			throw new DpssException(
					"The account 'admin' is reserved and cannot be registered");
		}

		Player p = null;

		// Validation on player account values
		try {
			p = new Player(username, password, (int) age, ipAddress, firstName,
					lastName, true);
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}

		// Account addition validation
		try {
			accountDatabase.addAccount(p);
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}
		
		logger.log("New account created: " + username );
	}

	@Override
	public synchronized void playerSignIn(String username, String password,
			String ipAddress, StringHolder firstName, StringHolder lastName,
			ShortHolder age) throws DpssException {
		
		logger.log("Player sign in request: " + ipAddress + ":" + username);

		Player p = null;
		try {
			p = accountDatabase.getAccount(username);
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}
		if (!(p.getPassword().trim().equalsIgnoreCase(password))) {
			throw new DpssException("The password is incorrect");
		}

		if (p.isOnline()) {
			throw new DpssException(
					"You are already signed in from another IP address");
		}

		p.setOnline(true);

		// Outs
		firstName.value = p.getFirstName();
		lastName.value = p.getLastName();
		age.value = p.getAge().shortValue();
		
		logger.log("Player signed in: " + username);
	}

	@Override
	public synchronized void playerSignOut(String username, String ipAddress)
			throws DpssException {
		
		logger.log("Player sign out request: " + ipAddress + ":" + username);
		try {
			accountDatabase.getAccount(username).setOnline(false);
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}
		logger.log("Player signed out: " + ipAddress + ":" + username);
	}

	@Override
	public synchronized void transferAccount(String username, String password,
			String oldIpAddress, String newIpAddress) throws DpssException {

		Player playerToTransfer = null;
		boolean temp = false;
		
		logger.log("Transfer account request for: " + oldIpAddress  + ":" + username + " to : " + newIpAddress);

		try {
			playerToTransfer = accountDatabase.getAccount(username);
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}

		// Loop through all repos and append their player info
		HashSet<HashMap<String, String>> allservers;
		try {
			allservers = Server.getRepo().getAllServers();
		} catch (ParserConfigurationException | SAXException | IOException e) {
			throw new DpssException(
					"Could not connect to the repo OR could not initialize socket");
		}

		// Find the details of the target server
		HashMap<String, String> targetServer = null;
		Iterator<HashMap<String, String>> serverItemIter = allservers
				.iterator();
		while (serverItemIter.hasNext()) {
			HashMap<String, String> serverItem = serverItemIter.next();

			// Make sure the user didn't want to transfer to it's own server
			if (serverItem.get("id").equalsIgnoreCase(
					Server.getServerProperties().getProperty("server.id"))) {
				temp = true;
			}

			if (serverItem.get("iprange").equalsIgnoreCase(
					Repo.getIpRange(newIpAddress))) {
				targetServer = serverItem;
			}
		}
		if((temp == true) && (targetServer == null)){
			throw new DpssException(
					"Source and target servers are the same");
		}

		// Validation: If no server was found
		if (targetServer == null) {
			throw new DpssException("No targer server with IP range '"
					+ Repo.getIpRange(newIpAddress) + "' was found in the repo");
		}

		// START PLAYER TRANSFER...
		DatagramSocket socket = null;

		synchronized (this) {

			// Send transfer request...
			try {
				socket = new DatagramSocket();
				socket.setSoTimeout(2000);
				UDPMessage sendMessage = new UDPMessage("transferPlayer",
						playerToTransfer, true, "");
				byte[] sendMessageBytes = sendMessage.getBytes();
				DatagramPacket sendPacket;
				sendPacket = new DatagramPacket(sendMessageBytes,
						sendMessageBytes.length, InetAddress.getLocalHost(),
						Integer.parseInt((String) targetServer.get("udpport")));
				socket.send(sendPacket);
			} catch (NumberFormatException | IOException e1) {
				e1.printStackTrace();
			}

			// Receive packet and analyze response
			DatagramPacket receivedPacket = new DatagramPacket(
					new byte[UDPMessage.BYTE_SIZE], UDPMessage.BYTE_SIZE);

			try {
				socket.receive(receivedPacket);
				UDPMessage receivedMessage = UDPMessage
						.getFromBytes(receivedPacket.getData());

				// Some error
				if (!receivedMessage.isSuccess()) {
					socket.close();
					throw new DpssException(receivedMessage.getErrorMessage());
				}

				// Successfully trasferred the account
				try {
					accountDatabase.removeAccount(username);
					logger.log("Account successfully transferred, removing account from current server");
					socket.close();
				} catch (Exception e) {
					socket.close();
					throw new DpssException(e.getMessage());
				}

			} catch (SocketTimeoutException e) {
				socket.close();
				throw new DpssException("The server '"
						+ targetServer.get("name") + "' timed out");
			} catch (IOException e) {
			}

		}

		

	}

	@Override
	public synchronized void getPlayersStatus(String adminUsername,
			String adminPassword, String ipAddress, StringHolder playersStatus)
			throws DpssException {
		
		logger.log("Get players status request from: " + ipAddress + ":" + adminUsername);

		if (!adminUsername.equalsIgnoreCase("admin")) {
			throw new DpssException("You are not an admin");
		}

		try {
			if (!accountDatabase.getAccount("admin").getPassword()
					.equalsIgnoreCase(adminPassword.trim())) {
				throw new DpssException("Invalid password");
			}
		} catch (Exception e) {
			throw new DpssException("Account does not exist: admin");
		}

		try {
			// Outs
			playersStatus.value = fetchPlayerStatusFromOtherServers();
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}
		
		logger.log("Players status fetched and returned to: " + ipAddress + ":" + adminUsername);

	}

	@Override
	public synchronized void suspendAccount(String adminUsername,
			String adminPassword, String ipAddress, String usernameToSuspend)
			throws DpssException {
		
		logger.log("Suspend account request: " + ipAddress + ":" + usernameToSuspend);

		if (!adminUsername.equalsIgnoreCase("admin")) {
			throw new DpssException("You are not an admin");
		}

		try {
			if (!accountDatabase.getAccount("admin").getPassword()
					.equalsIgnoreCase(adminPassword.trim())) {
				throw new DpssException("Invalid password");
			}
		} catch (Exception e) {
			throw new DpssException("Account does not exist: admin");
		}

		try {
			accountDatabase.removeAccount(usernameToSuspend);
			logger.log("Account suspended: " + ipAddress + ":" + usernameToSuspend);
		} catch (Exception e) {
			throw new DpssException(e.getMessage());
		}

	}

	private synchronized String fetchPlayerStatusFromOtherServers()
			throws Exception {

		// Hold own status
		String playersStatus = Server.getServerProperties().getProperty(
				"server.name")
				+ ": " + accountDatabase.getPlayersInfo();

		// Loop through all repos and append their player info
		HashSet<HashMap<String, String>> allservers;

		try {
			allservers = Server.getRepo().getAllServers();
		} catch (ParserConfigurationException | SAXException | IOException e) {
			throw new Exception(
					"Could not connect to the repo OR could not initialize socket");
		}

		DatagramSocket socket = new DatagramSocket();
		socket.setSoTimeout(1000);

		Iterator<HashMap<String, String>> serverItemIter = allservers
				.iterator();
		while (serverItemIter.hasNext()) {

			HashMap<String, String> serverItem = serverItemIter.next();

			// ping all other servers than the current one
			if (!(serverItem.get("id").equalsIgnoreCase(Server
					.getServerProperties().getProperty("server.id")))) {

				// Prepare send message
				UDPMessage sendMessage = new UDPMessage("getplayersinfo", null,
						true, "");
				byte[] sendMessageBytes = sendMessage.getBytes();

				DatagramPacket sendPacket = new DatagramPacket(
						sendMessageBytes, sendMessageBytes.length,
						InetAddress.getLocalHost(),
						Integer.parseInt((String) serverItem.get("udpport")));
				socket.send(sendPacket);

				// Collect the response
				DatagramPacket receivedPacket = new DatagramPacket(
						new byte[UDPMessage.BYTE_SIZE], UDPMessage.BYTE_SIZE);

				try {
					socket.receive(receivedPacket);
					UDPMessage receivedMessage = UDPMessage
							.getFromBytes(receivedPacket.getData());
					String receiveStr = new String(
							(String) receivedMessage.getData());
					playersStatus = playersStatus + " | "
							+ serverItem.get("name") + ": " + receiveStr;
				} catch (SocketTimeoutException e) {
					playersStatus = playersStatus + " | "
							+ serverItem.get("name") + " (timeout) ";
				}

			}
		}

		socket.close();
		return playersStatus;
	}

	/**
	 * ORB setter
	 * 
	 * @param orb
	 */
	public void setORB(ORB orb) {
		this.orb = orb;
	}

}
